package cn.zyl.demo.java.panel;

import javax.swing.JFrame;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.*;
import java.awt.Color;
import java.util.HashSet;
import java.util.Set;

/**
 * HighlightKeywords类说明:
 * ------------------------------------------------------------------------
 * <p><a href="http://www.cppblog.com/biao/archive/2010/02/05/107292.html">Java：写自己的文本编辑器(一): 高亮关键字</a></p>
 * <p>需要高亮的内容有:</p>
 * <p>1. 关键字, 如 public, int, true 等</p>
 * <p>2. 运算符, 如 +, -, *, /等</p>
 * <p>3. 数字</p>
 * <p>4. 高亮字符串, 如 "example of string"</p>
 * <p>5. 高亮单行注释</p>
 * <p>6. 高亮多行注释</p>
 * <p>Java中提供的多行文本编辑器有: JTextComponent, JTextArea, JTextPane, JEditorPane等, 都可以使用. 但是因为语法着色中文本要使用多种风格的样式, 所以这些文本编辑器的document要使用StyledDocument.</p>
 * <p>JTextArea使用的是PlainDocument, 此document不能进行多种格式的着色.</p>
 * <p>JTextPane, JEditorPane使用的是StyledDocument, 默认就可以使用.</p>
 * <p>为了实现语法着色, 可以继承自DefaultStyledDocument, 设置其为这些文本编辑器的documet, 或者也可以直接使用JTextPane, JEditorPane来做. 为了方便, 这里就直接使用JTextPane了.</p>
 * @author z.y.l
 * @version v1.0
 * @date 2020-09-07
 */
public class HighlightKeywords {

    public static void main(String[] args) {
        JFrame frame = new JFrame();
        JTextPane editor = new JTextPane();
        editor.getDocument().addDocumentListener(new SyntaxHighlighter(editor));
        frame.getContentPane().add(editor);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(500, 500);
        frame.setVisible(true);
    }
}
/**
 * 当文本输入区的有字符插入或者删除时, 进行高亮.
 * 要进行语法高亮, 文本输入组件的document要是styled document才行. 所以不要用JTextArea. 可以使用JTextPane.
 */
class SyntaxHighlighter implements DocumentListener {
    /** 关键字*/
    private final Set<String> keywords;
    /** 关键字样式*/
    private final Style keywordStyle;
    /** 默认样式*/
    private final Style normalStyle;

    public SyntaxHighlighter(JTextPane editor) {
        // 准备着色使用的样式
        keywordStyle = ((StyledDocument) editor.getDocument()).addStyle("Keyword_Style", null);
        normalStyle = ((StyledDocument) editor.getDocument()).addStyle("Keyword_Style", null);
        StyleConstants.setForeground(keywordStyle, Color.RED);
        StyleConstants.setForeground(normalStyle, Color.BLACK);
        // 准备关键字
        keywords = new HashSet<>();
        keywords.add("public");
        keywords.add("protected");
        keywords.add("private");
        keywords.add("_int9");
        keywords.add("float");
        keywords.add("double");
    }
    public void colouring(StyledDocument doc, int pos, int len) throws BadLocationException {
        // 取得插入或者删除后影响到的单词.
        // 例如"public"在b后插入一个空格, 就变成了:"pub lic", 这时就有两个单词要处理:"pub"和"lic"
        // 这时要取得的范围是pub中p前面的位置和lic中c后面的位置
        int start = indexOfWordStart(doc, pos);
        int end = indexOfWordEnd(doc, pos + len);

        char ch;
        while (start < end) {
            ch = getCharAt(doc, start);
            if (Character.isLetter(ch) || ch == '_') {
                // 如果是以字母或者下划线开头, 说明是单词
                // pos为处理后的最后一个下标
                start = colouringWord(doc, start);
            } else {
                SwingUtilities.invokeLater(new ColouringTask(doc, start, 1, normalStyle));
                ++start;
            }
        }
    }
    /**
     * 对单词进行着色, 并返回单词结束的下标.
     *
     * @param doc 文本
     * @param pos 变化前光标的位置
     * @return 下标
     * @throws BadLocationException 异常
     */
    public int colouringWord(StyledDocument doc, int pos) throws BadLocationException {
        int wordEnd = indexOfWordEnd(doc, pos);
        String word = doc.getText(pos, wordEnd - pos);
        if (keywords.contains(word)) {
            // 如果是关键字, 就进行关键字的着色, 否则使用普通的着色.
            // 这里有一点要注意, 在insertUpdate和removeUpdate的方法调用的过程中, 不能修改doc的属性.
            // 但我们又要达到能够修改doc的属性, 所以把此任务放到这个方法的外面去执行.
            // 实现这一目的, 可以使用新线程, 但放到swing的事件队列里去处理更轻便一点.
            SwingUtilities.invokeLater(new ColouringTask(doc, pos, wordEnd - pos, keywordStyle));
        } else {
            SwingUtilities.invokeLater(new ColouringTask(doc, pos, wordEnd - pos, normalStyle));
        }
        return wordEnd;
    }
    /**
     * 取得在文档中下标在pos处的字符.
     *
     * 如果pos为doc.getLength(), 返回的是一个文档的结束符, 不会抛出异常. 如果pos<0, 则会抛出异常.
     * 所以pos的有效值是[0, doc.getLength()]
     *
     * @param doc 文本
     * @param pos 变化前光标的位置
     * @return 字符
     * @throws BadLocationException 异常
     */
    public char getCharAt(Document doc, int pos) throws BadLocationException {
        return doc.getText(pos, 1).charAt(0);
    }
    /**
     * 取得下标为pos时, 它所在的单词开始的下标. Â±wor^dÂ± (^表示pos, Â±表示开始或结束的下标)
     *
     * @param doc 文本
     * @param pos 变化前光标的位置
     * @return 下标
     * @throws BadLocationException 异常
     */
    public int indexOfWordStart(Document doc, int pos) throws BadLocationException {
        // 从pos开始向前找到第一个非单词字符.
        //for (; pos > 0 && isWordCharacter(doc, pos - 1); --pos) { }
        while (pos > 0 && isWordCharacter(doc, pos - 1)){
            --pos;
        }
        return pos;
    }
    /**
     * 取得下标为pos时, 它所在的单词结束的下标. Â±wor^dÂ± (^表示pos, Â±表示开始或结束的下标)
     *
     * @param doc 文本
     * @param pos 变化前光标的位置
     * @return 下标
     * @throws BadLocationException 异常
     */
    public int indexOfWordEnd(Document doc, int pos) throws BadLocationException {
        // 从pos开始向前找到第一个非单词字符.
        //for (; isWordCharacter(doc, pos); ++pos) { }
        while (isWordCharacter(doc, pos)){
            ++pos;
        }
        return pos;
    }
    /**
     * 如果一个字符是字母, 数字, 下划线, 则返回true.
     *
     * @param doc 文本
     * @param pos 变化前光标的位置
     * @return 是否是字母, 数字, 下划线
     * @throws BadLocationException 异常
     */
    public boolean isWordCharacter(Document doc, int pos) throws BadLocationException {
        char ch = getCharAt(doc, pos);
        if (Character.isLetter(ch) || Character.isDigit(ch) || ch == '_') { return true; }
        return false;
    }
    @Override
    public void changedUpdate(DocumentEvent e) {

    }
    @Override
    public void insertUpdate(DocumentEvent e) {
        try {
            colouring((StyledDocument) e.getDocument(), e.getOffset(), e.getLength());
        } catch (BadLocationException e1) {
            e1.printStackTrace();
        }
    }
    @Override
    public void removeUpdate(DocumentEvent e) {
        try {
            // 因为删除后光标紧接着影响的单词两边, 所以长度就不需要了
            colouring((StyledDocument) e.getDocument(), e.getOffset(), 0);
        } catch (BadLocationException e1) {
            e1.printStackTrace();
        }
    }
    /** 完成着色任务*/
    private static class ColouringTask implements Runnable {
        private final StyledDocument doc;
        private final Style style;
        /** 指变化前光标的位置.*/
        private final int pos;
        /** 指变化的字符数.*/
        private final int len;
        public ColouringTask(StyledDocument doc, int pos, int len, Style style) {
            this.doc = doc;
            this.pos = pos;
            this.len = len;
            this.style = style;
        }
        @Override
        public void run() {
            try {
                // 这里就是对字符进行着色
                doc.setCharacterAttributes(pos, len, style, true);
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
    }
}